import os
import time
import signal
import logging
from multiprocessing import Process

from .library.json_helper import JsonHelper
from .library.signal_pipe import SignalPipe
from .library.coverage_helper import setup_coverage
from .config import Config, DirectoryManager
from .scheduler import Scheduler
from .webui import WebUI
from .timer import TimerManager


LOG = logging.getLogger(__name__)


SIGNALS = [signal.SIGINT, signal.SIGHUP, signal.SIGCHLD]


def _signame(sig):
    """
    >>> _signame(signal.SIGINT)
    'SIGINT'
    """
    try:
        return signal.Signals(sig).name
    except (ValueError, TypeError, AttributeError):
        return str(sig)


class Worker:

    def __init__(self, name, cls, config, **kwargs):
        self.name = name
        self.cls = cls
        self.config = config
        self.kwargs = kwargs

    def main(self):
        LOG.info(f'tinyops-{self.name} pid={os.getpid()} started')
        # reset signals
        for s in SIGNALS:
            signal.signal(s, signal.SIG_DFL)
        setup_coverage()
        worker = self.cls(self.config, **self.kwargs)
        try:
            worker.main()
        except KeyboardInterrupt:
            pass  # ignore
        LOG.info(f'tinyops-{self.name} pid={os.getpid()} exited')


class WorkerGroup:

    def __init__(self, config: Config):
        self.config = config

    def main(self):
        os.setpgrp()  # become process group leader
        pid = os.getpid()
        pgid = os.getpgrp()
        signal_pipe = SignalPipe()
        for s in SIGNALS:
            signal.signal(s, signal_pipe.handler)
        LOG.info(f'tinyops-worker-group pid={pid} started')
        reloader = (pid, signal.SIGHUP)
        workers = [
            Worker('webui', WebUI, self.config, reloader=reloader),
            Worker('timer', TimerManager, self.config),
            Worker('scheduler', Scheduler, self.config),
        ]
        for worker in workers:
            proc = Process(target=worker.main, name=f"tinyops-{worker.name}")
            proc.start()
        ppid = os.getppid()
        while True:
            # wait signal, then kill the group and exit
            sig = signal_pipe.wait(timeout=1.0)
            if sig is None:
                if os.getppid() == ppid:
                    continue
                LOG.warning(f'parent changed, shutting down')
            else:
                LOG.info(f'receive signal {_signame(sig)}')
                if sig == signal.SIGHUP:
                    LOG.info("reload config and restart workers")
                    time.sleep(1.0)  # wait webui request finishs
                elif sig == signal.SIGCHLD:
                    LOG.warning("worker process exited, restart workers")
            # kill all child process in the group
            LOG.info(f'tinyops-worker-group pid={pid} shutting down')
            try:
                os.killpg(pgid, signal.SIGKILL)
            except (ProcessLookupError, PermissionError):
                # https://stackoverflow.com/questions/12521705/why-would-killpg-return-not-permitted-when-ownership-is-correct
                LOG.debug("tinyops-worker-group already exited")
            break


class Supervisor:

    def __init__(self, config_filepath: str = None):
        self.config_filepath = config_filepath

    def _process_config(self) -> Config:
        config = Config.load(filepath=self.config_filepath)
        if config.debug:
            print(JsonHelper.pretty(config.raw, shorten=True))
        dm = DirectoryManager(config)
        # task executor will read the config file
        with open(dm.current_config(), 'w') as f:
            f.write(config.to_json())
        return config

    def main(self):
        while True:
            config = self._process_config()
            LOG.info('starting tinyops-worker-group')
            worker_group = WorkerGroup(config)
            proc = Process(name='tinyops-worker-group', target=worker_group.main)
            proc.start()
            proc_pgid = proc.pid
            try:
                # wait worker_group setpgrp and get it's pgid
                for i in range(100):
                    proc_pgid = os.getpgid(proc.pid)
                    time.sleep(0.01)
                proc.join()
            finally:
                try:
                    os.killpg(proc_pgid, signal.SIGKILL)
                except (ProcessLookupError, PermissionError):
                    # https://stackoverflow.com/questions/12521705/why-would-killpg-return-not-permitted-when-ownership-is-correct
                    LOG.debug("tinyops-worker-group already exited")
            time.sleep(1)
